package tcpclient

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"github.com/Unixeno/TheMoon/authentication"
	"github.com/Unixeno/TheMoon/logging"
	"github.com/Unixeno/TheMoon/model"
	"github.com/Unixeno/TheMoon/pool"
	"github.com/Unixeno/TheMoon/radio/packet"
	"net"
	"sync/atomic"
	"time"
)

type TCPClient struct {
	ctx        context.Context // 全局context
	errChan    chan error
	state      uint32 // 自旋锁，0=等待，1=可链接
	config     *ClientConfig
	planetID   string
	connection net.Conn
	sendQueue  chan []byte
}

type ClientConfig struct {
	Addr             string
	NodeID           string
	HandshakeTimeout time.Duration
	ReadDeadline     time.Duration
	WriteDeadline    time.Duration
	CAFile           string // 用于服务端验证的ca文件，不设置时使用系统root ca
	CertFile         string // 客户端证书文件，不设置时则不使用客户端证书校验
	PrivateKeyFile   string // 客户端证书密钥
}

var logger = logging.GetLogger("tcp-client")

func CreateClient(config *ClientConfig, planetID string) (*TCPClient, ) {
	return &TCPClient{
		config:   config,
		planetID: planetID,
	}
}

func (c *TCPClient) connect() {
	logger.Debug("connect to planet")
	rootCAPool := x509.NewCertPool()
	var tlsConfig *tls.Config
	if len(c.config.CertFile) != 0 {
		// 使用客户端证书检查
		cer, err := tls.LoadX509KeyPair(c.config.CertFile, c.config.PrivateKeyFile)
		if err != nil {
			logger.WithError(err).Error("failed to load client certificate")
			c.errChan <- err
			return
		}
		logger.WithField("cert", c.config.CertFile).WithField("private", c.config.PrivateKeyFile).
			Debug("client certificate successfully loaded")
		tlsConfig = &tls.Config{
			Certificates: []tls.Certificate{cer},
		}
	} else {
		tlsConfig = &tls.Config{}
	}

	if 0 != len(c.config.CAFile) {
		ca, err := authentication.ReadCertificate(c.config.CAFile)
		if err != nil {
			logger.WithField("caFile", c.config.CAFile).Error("filed to load ca for client authentication")
			c.errChan <- err
			return
		} else {
			rootCAPool.AddCert(ca)
			logger.WithField("caFile", c.config.CAFile).Debug("load ca file succeeded")
		}
	} else {
		// 未指定ca时使用系统ca文件
		logger.Debug("ca file not set, use system ca instead")
		rootCAPool, _ = x509.SystemCertPool() // 函数只在windows上返回错误
	}

	tlsConfig.RootCAs = rootCAPool
	tlsConfig.ServerName = c.planetID

	var conn net.Conn
	var err error
	for retryCount := 1; ; retryCount++ {
		dialer := &net.Dialer{Timeout: c.config.HandshakeTimeout}
		conn, err = tls.DialWithDialer(dialer, "tcp", c.config.Addr, tlsConfig)
		if err != nil {
			logger.WithError(err).Error("dial to planet failed")
		} else {
			// 连接成功
			c.connection = conn
			atomic.StoreUint32(&c.state, 1)
			return
		}
		if retryCount > 3 {
			break
		} else {
			logger.Warningf("retry after %d seconds", retryCount*3)
			time.Sleep(time.Duration(retryCount) * 3 * time.Second)
		}
	}

	c.errChan <- errors.New("reach the maximum retry times")
	return
}

func (c *TCPClient) Init(ctx context.Context) chan error {
	logger.Debug("init tcp client")
	c.ctx = ctx
	c.sendQueue = make(chan []byte, 16)
	c.errChan = make(chan error)
	go c.connect()

	go c.sendToPlanet()
	return c.errChan
}

func (c *TCPClient) Send(message *model.Message) error {
	logger.WithField("m", message.Header.Source).Debug("send")
	buffer := pool.Fixed64KPool.Get().([]byte)
	n, err := packet.WritePacket(buffer, message)
	if err == packet.BufferTooSmall {
		logger.Error("message too long, ignore")
		return err
	}
	c.sendQueue <- buffer[:n]
	return nil
}

func (c *TCPClient) Receive() (*model.Message, error) {
	if err := c.wait(); err != nil {
		return nil, err
	}
	message, err := packet.ReadPacket(c.connection, c.config.ReadDeadline)
	if err != nil {
		return nil, err
	}
	return message, nil
}

func (c *TCPClient) sendToPlanet() {
	for chunk := range c.sendQueue {
		logger.Debug("send packet to client")

		if c.wait() != nil {
			return
		}

		_ = c.connection.SetWriteDeadline(time.Now().Add(c.config.WriteDeadline))
		_, err := c.connection.Write(chunk)
		if err != nil {
			logger.WithError(err).Error()
			return
		}
		pool.Fixed64KPool.Put(chunk[:cap(chunk)])
	}
}

func (c *TCPClient) wait() error {
	for {
		if atomic.LoadUint32(&c.state) == 1 {
			return nil
		}
		select {
		case <-time.After(time.Second):
			continue
		case <-c.ctx.Done():
			return c.ctx.Err()
		}
	}
}
